﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text.RegularExpressions;
using System.Threading;
using DotNet.DevOps.Tasks.Util;
using Microsoft.Build.Framework;

namespace DotNet.DevOps.Tasks.Oracle
{
    public interface IOracleScript
    {
        string Files { get; set; }
        string Database { get; set; }
        string User { get; set; }
        string Password { get; set; }
        bool FailOnLogonError { get; set; }
        bool FailOnScriptError { get; set; }
        int Timeout { get; set; }
    }

    /// <summary>
    /// Runs a sql script against an Oracle database.
    /// Can fail on script error if FailOnScriptError is set to true. 
    /// 
    /// Requirements:
    /// * Oracle driver with SQLPlus, must be installed.
    /// 
    /// Default behaviour:
    /// * Fails if logon is not possible.
    /// * Do not fail if the script has errors.
    /// * No timeout.
    /// 
    /// </summary>
    /// <example>
    /// import-module .\DotNet.DevOps.Tasks.dll
    /// 
    /// $task = New-Object DotNet.DevOps.Tasks.Oracle.OracleScript -Property @{
    ///                                                                 Files="MyScript.sql;MySecondScript.sql" 
    ///                                                                 Database="TEST"
    ///                                                                 User="SYS" 
    ///                                                                 Password="SYSPASSWORD"  
    ///                                                                 Timeout="10000"
    ///                                                             }
    /// $result = $task.Execute()
    /// </example>
    /// <example>
    /// <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    /// 
    ///  <Import Project="$(libs)\DotNet.DevOps.Tasks.Targets" />
    /// 
    ///  <Target Name="Build">
    /// 
    ///    <OracleScript    Files="MyScript.sql;MySecondScript.sql" 
    ///                     Database="TEST"
    ///                     User="SYS" 
    ///                     Password="SYSPASSWORD"  
    ///                     Timeout="10000" />
    /// 
    ///  </Target>
    /// 
    /// </Project>
    /// </example>
    public class OracleScript : ITask, IOracleScript
    {
        public IBuildEngine BuildEngine { get; set; }
        public ITaskHost HostObject { get; set; }

        /// <summary>
        /// Script files to run separated by ;
        /// </summary>
        [Required]
        public string Files { get; set; }

        /// <summary>
        /// TNSNAME of the database to run the script against.
        /// </summary>
        [Required]
        public string Database { get; set; }

        /// <summary>
        /// User name to connect as. Ex. SAM, SYS.
        /// </summary>
        [Required]
        public string User { get; set; }

        /// <summary>
        /// Password.
        /// </summary>
        [Required]
        public string Password { get; set; }

        /// <summary>
        /// Should the task fail when a logon error occours. Default true.
        /// </summary>
        [Standard]
        public bool FailOnLogonError { get; set; }

        /// <summary>
        /// Should the task fail when an error occours in a script. Default false.
        /// </summary>
        [Standard]
        public bool FailOnScriptError { get; set; }

        /// <summary>
        /// Timeout for script to run to completion in milliseconds. Negative values means unlimited. Default unlimited.
        /// </summary>
        [Standard]
        public int Timeout { get; set; }

        /// <summary>
        /// This regex will capture all XXX-000(000) errors which covers the stanard oracle error code formats.
        /// </summary>
        private readonly Regex oraRegex = new Regex(@"^\S{3}-\d{3,6}:.*$", RegexOptions.Compiled | RegexOptions.Multiline);

        private ILog Log { get; set; }

        public OracleScript()
        {
            Init();
            Log = new MSBuildLogger(this);
        }

        public OracleScript(ILog log)
        {
            Init();
            Log = log;
        }

        private void Init()
        {
            FailOnLogonError = true;
            FailOnScriptError = false;
            Timeout = -1;
        }

        public bool Execute()
        {
            try
            {
                var files = Files.GetValidFilenames();

                files.ToList().ForEach(RunScript);
            }
            catch (Exception ex)
            {
                Log.LogError(ex);
                return false;
            }

            return true;
        }

        private void RunScript(string filename)
        {
            var wrapperFile = GetWrapperSqlScript(filename);

            var sqlPlusProcess = CreateProcess(filename, wrapperFile);

            RunProcess(sqlPlusProcess);

            CheckProcessHasCompleted(sqlPlusProcess, wrapperFile);

            var output = GetProcessOutput(sqlPlusProcess, wrapperFile);

            ValidateOutput(output);
        }

        private string GetWrapperSqlScript(string filename)
        {
            var wrapperFile = Path.Combine(Path.GetDirectoryName(filename), "Wrapper.sql");
            File.Copy(filename, wrapperFile, true);
            File.AppendAllLines(wrapperFile, new[] { "exit" });
            return wrapperFile;
        }

        private Process CreateProcess(string filename, string wrapperFile)
        {
            var arguments = string.Format("{0} @\"{1}\"", GenerateConnectionString(), wrapperFile);
            Log.LogInfo(string.Format("Running script {0} using {1}.", filename, wrapperFile));

            return new Process
            {
                EnableRaisingEvents = false,
                StartInfo = new ProcessStartInfo
                {
                    CreateNoWindow = true,
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    WorkingDirectory = Environment.CurrentDirectory,
                    FileName = "sqlplus.exe",
                    Arguments = arguments
                }
            };
        }

        private void RunProcess(Process process)
        {
            process.Start();

            if (Timeout >= 0)
            {
                process.WaitForExit(Timeout);
            }
            else
            {
                process.WaitForExit();
            }
        }

        private void CheckProcessHasCompleted(Process process, string filename)
        {
            if (process.HasExited == false)
            {
                process.Kill();
                GetProcessOutput(process, filename);
                throw new Exception(string.Format("Timeout has occoured waiting for the script {0} to complete. Consider changing the script or setting another timeout value for the task.", filename));
            }
        }

        private string GetProcessOutput(Process process, string filename)
        {
            var output = string.Format("{0} {1}", process.StandardOutput.ReadToEnd(), process.StandardError.ReadToEnd());
            Log.LogInfo(output);

            File.Delete(filename);

            return output;
        }

        private string GenerateConnectionString()
        {
            return string.Format("{0}/{1}@{2} {3}", User, Password, Database, (User.ToLower() == "sys" ? "AS SYSDBA" : ""));
        }

        private void ValidateOutput(string output)
        {
            var matches = oraRegex.Matches(output);

            foreach(Match match in matches)
            {
                CheckLogonError(match.Value);
                CheckErrors(match.Value);
            }
        }

        private void CheckLogonError(string error)
        {
            if(error.Contains("ORA-01017") || error.Contains("logon denied"))
            {
                if (FailOnLogonError == true)
                {
                    throw new Exception(error);
                }

                Log.LogWarning(error);
            }
        }

        private void CheckErrors(string error)
        {
            if(FailOnScriptError == true)
            {
                throw new Exception(error);
            }

            Log.LogWarning(error);
        }
    }
}
